package flinty.autocomplete.combobox;

import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JComboBox;
import javax.swing.UIManager;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;

public class AutoCompleteComboBoxDocument extends PlainDocument {

	private static final long serialVersionUID = 9206258902573168129L;

	private final JTextComponent textComponent;

	private final JComboBox comboBox;

	private final BaseAutoCompleteComboBoxModel<?> model;

	private boolean processInsert;

	private boolean processRemove;

	private String previousPattern;

	private static Logger logger = AutoCompleteComboBoxDecorator
			.createLogger(AutoCompleteComboBoxDocument.class);

	public AutoCompleteComboBoxDocument(final JComboBox control,
			final BaseAutoCompleteComboBoxModel<?> aModel) {
		comboBox = control;
		model = aModel;
		textComponent = (JTextComponent) comboBox.getEditor()
				.getEditorComponent();
		textComponent.setDocument(this);
		textComponent.addKeyListener(new KeyAdapter() {

			@Override
			public void keyPressed(KeyEvent e) {
				if (e.getKeyCode() == KeyEvent.VK_ENTER) {
					logger.fine("enter key pressed");
					e.consume();
					if (comboBox.isPopupVisible()) {
						comboBox.setPopupVisible(false);
					}
					return;
				}
				processInsert = !e.isActionKey()
						&& e.getKeyCode() != KeyEvent.VK_ENTER
						&& e.getKeyCode() != KeyEvent.VK_BACK_SPACE
						&& e.getKeyCode() != KeyEvent.VK_ESCAPE
						&& e.getKeyCode() != KeyEvent.VK_DELETE;
				processRemove = e.getKeyCode() == KeyEvent.VK_BACK_SPACE
						|| e.getKeyCode() == KeyEvent.VK_DELETE;
				aModel.enableProcessing();
				logger.fine(String
						.format("key pressed %s, processRemove = %s, processInsert = %s",
								"" + e.getKeyChar(),
								Boolean.toString(processRemove),
								Boolean.toString(processInsert)));
			}
		});
	}

	public void setText(String newText) {
		try {
			int length = getLength();
			String actual = getText(0, length);
			logger.fine(String.format("setting text: new text = %s, "
					+ "actual text = %s, length = %s", newText, actual,
					Integer.toString(length)));
			previousPattern = null;
			if (actual == null ? newText != null : !actual.equals(newText)) {
				super.remove(0, length);
				super.insertString(0, newText, null);
				logger.fine(String.format(
						"text updated: actual = %s, length = %s",
						getText(0, getLength()), Integer.toString(getLength())));
			}
		} catch (BadLocationException e1) {
			logger.log(Level.SEVERE, "error ocured", e1);
		}

	}

	private boolean updateModel(final String newPattern)
			throws BadLocationException {
		logger.fine("updateModel() START, pattern = " + newPattern);
		String pattern = newPattern;
		if (pattern != null && pattern.trim().isEmpty()) {
			pattern = null;
		}
		logger.fine("setPattern() START");
		boolean result = model.setPattern(pattern);
		logger.fine("setPattern() END, result = " + result);
		if (!result) {
			return false;
		}
		model.setUpdatePopup(true);
		if (comboBox.isDisplayable()) {
			logger.fine("hiding popup");
			comboBox.setPopupVisible(false);
			if (model.getSize() > 0 && !comboBox.isPopupVisible()) {
				logger.fine("showing popup");
				comboBox.setPopupVisible(true);
			}
		}
		model.setUpdatePopup(false);
		return true;
	}

	@Override
	public void remove(int offs, int len) throws BadLocationException {
		logger.fine(String.format("enterring remove: offs = %s, length = %s",
				Integer.toString(offs), Integer.toString(len)));
		if (!processRemove) {
			logger.fine("skip remove");
			processRemove = false;
			return;
		}
		processRemove = false;
		String text = getText(0, getLength());
		if (offs + len <= text.length()) {
			String newText = text.substring(0, offs)
					+ (offs + len < text.length() ? text.substring(offs + len)
							: "");
			logger.fine(String.format(
					"offs = %s, len = %s, getText = %s,  newText= %s", offs,
					len, text, newText));
			if (newText.equals(previousPattern)) {
				logger.fine("new pattern equals previous one");
				return;
			}
			logger.fine("calling updateModel()");
			if (updateModel(newText)) {
				setText(newText);
				clearSelection();
				previousPattern = newText;
				logger.fine("new pattern " + newText);
				return;
			}
			logger.fine("new pattern is invalid");
			UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}
		logger.fine("start position is less than zero");
		UIManager.getLookAndFeel().provideErrorFeedback(null);
		clearSelection();
	}

	@Override
	public void insertString(int offs, String str, AttributeSet a)
			throws BadLocationException {
		logger.fine(String.format("enterring insert: offs = %s, string = %s",
				Integer.toString(offs), str));
		if (!processInsert) {
			processRemove = false;
			logger.fine("key is not charkey");
			return;
		}
		processInsert = false;
		String text = getText(0, getLength());
		String newText = text.substring(0, offs) + str
				+ (text.length() > offs ? text.substring(offs) : "");
		logger.fine(String.format(
				"offs = %s, str = %s, getText = %s,  newText= %s", offs, str,
				text, newText));
		if (newText.equals(previousPattern)) {
			logger.fine("new pattern equals previous one");
			return;
		}
		logger.fine("calling updateModel()");
		if (updateModel(newText)) {
			setText(newText);
			clearSelection();
			logger.fine("new pattern " + newText);
			previousPattern = newText;
			return;
		}
		logger.fine("new pattern is invalid");
		UIManager.getLookAndFeel().provideErrorFeedback(null);
	}

	@Override
	public void replace(int offset, int length, String text, AttributeSet attrs)
			throws BadLocationException {
		logger.fine(String.format(
				"enterring replace: offs = %s, length = %s, string = %s",
				Integer.toString(offset), Integer.toString(length), text));
		if (!processRemove && !processInsert) {
			logger.fine("key is not charkey");
			return;
		}
		processRemove = false;
		processInsert = false;
		String oldText = getText(0, getLength());
		if (offset + length <= oldText.length()) {
			String newText = oldText.substring(0, offset)
					+ (offset + length < oldText.length() ? oldText
							.substring(offset + length) : "");
			newText = newText.substring(0, offset)
					+ text
					+ (newText.length() > offset ? newText.substring(offset)
							: "");
			logger.fine(String.format("newText= %s", newText));
			if (newText.equals(previousPattern)) {
				logger.fine("new pattern equals previous one");
				return;
			}
			logger.fine("calling updateModel()");
			if (updateModel(newText)) {
				setText(newText);
				clearSelection();
				previousPattern = newText;
				logger.fine("pattern updated " + newText);
				return;
			}
			logger.fine("new pattern is invalid");
			UIManager.getLookAndFeel().provideErrorFeedback(null);
			return;
		}
		logger.fine("start position is less than zero");
	}

	public JComboBox getComboBox() {
		return comboBox;
	}

	void selectAllText() {
		int i = textComponent.getText().length();
		textComponent.setSelectionStart(0);
		textComponent.setSelectionEnd(i);
	}

	private void clearSelection() {
		int i = textComponent.getText().length();
		textComponent.setSelectionStart(i);
		textComponent.setSelectionEnd(i);
	}
}
